﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Win32;

namespace TfsRollUp.WixCustomAction
{
    /// <summary>
    /// Create and remove hardlinks as defined in the msi table HardLink
    /// </summary>
    public static class HardLinks
    {
        /// <summary>
        /// Resolve destination as filename only or prefixed with a HKLM key also resolved
        /// </summary>
        /// <param name="hklm">HKLM key</param>
        /// <param name="session">msi session</param>
        /// <param name="destination">destination as read from HardLink</param>
        /// <param name="formattedSource">a session.Format string resolved to install full filename</param>
        /// <returns>A full filename</returns>
        private static string ResolvePath(RegistryKey hklm, Session session, string destination, string formattedSource)
        {
            var formattedDestination = destination;
            if (destination.StartsWith("HKLM", true, CultureInfo.InvariantCulture))
            {
                var parts = destination.Split('\\');
                if (parts == null || parts.Length <= 2)
                {
                    session.Log("ResolvePath: Ups, Something is wrong with the destination:" + destination);
                    return null;
                }
                string subKey = "";
                for (int j = 1; j < parts.Length - 2; j++)
                {
                    subKey += parts[j] + "\\";
                }
                using (var key = hklm.OpenSubKey(subKey))
                {
                    if (key == null)
                    {
                        session.Log(string.Format("ResolvePath: Ups, registry key not found for subkey: '{0}' ", subKey));
                        return null;
                    }
                    var value = parts[parts.Length - 2];
                    var path = (string)key.GetValue(value);
                    session.Log(string.Format("ResolvePath: Path read from key {0} value: {1} is: {2} ", subKey, value, path));
                    formattedDestination = Path.Combine(path, parts[parts.Length - 1]);
                }
            }
            else
            {
                formattedDestination = Path.Combine(new FileInfo(formattedSource).DirectoryName, destination);
            }
            return formattedDestination;
        }

        /// <summary>
        /// Creates hardlinks as defined in the HardLink custom msi table
        /// </summary>
        /// <param name="session"></param>
        /// <returns></returns>
        [CustomAction]
        public static ActionResult CreateHardLinksImmediate(Session session)
        {
            session.Log("CreateHardLinksImmediate: Hello");
            try
            {
                var hardLinks = (List<string>)session.Database.ExecuteStringQuery("SELECT `Source`,`Destination` FROM `HardLink`");
                if (hardLinks == null || hardLinks.Count == 0)
                {
                    session.Log("CreateHardLinksImmediate: No hardlinks were found in the table 'HardLink'");
                    return ActionResult.NotExecuted;
                }
                var msg = string.Format("CreateHardLinksImmediate: {0} hardlinks were found in the table 'HardLink'", hardLinks.Count);
                session.Log(msg);

                var fileList = new List<string>();
                var hklm = Registry.LocalMachine;
                if (hklm == null)
                {
                    session.Log("CreateHardLinksImmediate: Ups, HKLM is null");
                    return ActionResult.NotExecuted;
                }
                for (int i = 0; i < hardLinks.Count / 2; i++)
                {
                    var source = hardLinks[i * 2];

                    // Expand the file id to the actual install file path and name
                    var formattedSource = session.Format("[#" + source + "]");
                    var destination = hardLinks[(i * 2) + 1];
                    string formattedDestination = ResolvePath(hklm, session, destination, formattedSource);

                    if (!string.IsNullOrWhiteSpace(formattedSource) && !string.IsNullOrWhiteSpace(formattedDestination))
                    {
                        fileList.Add(formattedSource);
                        fileList.Add(formattedDestination);
                    }
                }
                var customActionData = new CustomActionData();
                customActionData.AddObject<List<string>>("HardLink", fileList);

                session.Log("CreateHardLinksImmediate: Calling DoAction...");
                session.DoAction("CreateHardLinksDeferred", customActionData);
            }
            catch (Exception ex)
            {
                session.Log("CreateHardLinksImmediate: Exception caught:" + ex);
            }
            return ActionResult.Success;
        }

        /// <summary>
        /// Removes hardlinks as defined in the HardLink custom msi table
        /// </summary>
        /// <param name="session"></param>
        /// <returns></returns>
        [CustomAction]
        public static ActionResult RemoveHardLinksImmediate(Session session)
        {
            session.Log("RemoveHardLinksImmediate: Hello");
            try
            {
                var hardLinks = (List<string>)session.Database.ExecuteStringQuery("SELECT `Source`,`Destination` FROM `HardLink`");
                if (hardLinks == null || hardLinks.Count == 0)
                {
                    session.Log("RemoveHardLinksImmediate: No hardlinks were found in the table 'HardLink'");
                    return ActionResult.NotExecuted;
                }
                var msg = string.Format("RemoveHardLinksImmediate: {0} hardlinks were found in the table 'HardLink'", hardLinks.Count);
                session.Log(msg);

                var fileList = new List<string>();
                var hklm = Registry.LocalMachine;
                if (hklm == null)
                {
                    session.Log("RemoveHardLinksImmediate: Ups, HKLM is null");
                    return ActionResult.NotExecuted;
                }
                for (int i = 0; i < hardLinks.Count / 2; i++)
                {
                    var source = hardLinks[i * 2];

                    // Expand the file id to the actual install file path and name
                    var formattedSource = session.Format("[#" + source + "]");
                    var destination = hardLinks[(i * 2) + 1];
                    string formattedDestination = ResolvePath(hklm, session, destination, formattedSource);

                    if (!string.IsNullOrWhiteSpace(formattedSource) && !string.IsNullOrWhiteSpace(formattedDestination))
                    {
                        fileList.Add(formattedDestination);
                    }
                }
                var customActionData = new CustomActionData();
                customActionData.AddObject<List<string>>("HardLink", fileList);

                session.Log("RemoveHardLinksImmediate: Calling DoAction...");
                session.DoAction("RemoveHardLinksDeferred", customActionData);
            }
            catch (Exception ex)
            {
                session.Log("RemoveHardLinksImmediate: Exception caught:" + ex);
            }
            return ActionResult.Success;
        }

        /// <summary>
        /// Called as deferred CA from msi, because it changes system state
        /// </summary>
        /// <param name="session"></param>
        /// <returns></returns>
        [CustomAction]
        public static ActionResult CreateHardLinksDeferred(Session session)
        {
            session.Log("CreateHardLinksDeferred: Hello");
            if (session.CustomActionData == null)
            {
                session.Log("CreateHardLinksDeferred: No CustomActionData passed on");
                return ActionResult.NotExecuted;
            }
            var hardLinks = session.CustomActionData.GetObject<List<string>>("HardLink");
            if (hardLinks == null || hardLinks.Count == 0)
            {
                session.Log("CreateHardLinksDeferred: No hardlinks passed via CustomActionData");
                return ActionResult.NotExecuted;
            }
            for (int i = 0; i < hardLinks.Count / 2; i++)
            {
                var source = hardLinks[2 * i];
                var destination = hardLinks[(2 * i) + 1];
                session.Log(string.Format("CreateHardLinksDeferred: Source:{0} Destination:{1}", source, destination));
                if (File.Exists(destination))
                {
                    session.Log("CreateHardLinksDeferred: The destination file '{0}' already exists", destination);
                    continue;
                }
                if (!File.Exists(source))
                {
                    session.Log("CreateHardLinksDeferred: The source file '{0}' does not exist", source);
                    continue;
                }
                if (NativeMethods.CreateHardLink(destination, source, IntPtr.Zero) == false)
                {
                    var msg = string.Format("CreateHardLinksDeferred: Error:{0} occurred for CreateLink.", Marshal.GetLastWin32Error());
                    session.Log(msg);
                }
                else
                {
                    var msg = string.Format("CreateHardLinksDeferred: Hardlink successfully created for :{0}", destination);
                    session.Log(msg);
                }
            }
            return ActionResult.Success;
        }

        /// <summary>
        /// Called as deferred CA from msi, because it changes system state
        /// </summary>
        /// <param name="session"></param>
        /// <returns></returns>
        [CustomAction]
        public static ActionResult RemoveHardLinksDeferred(Session session)
        {
            session.Log("RemoveHardLinksDeferred: Hello");
            if (session.CustomActionData == null)
            {
                session.Log("RemoveHardLinksDeferred: No CustomActionData passed on");
                return ActionResult.NotExecuted;
            }
            var hardLinks = session.CustomActionData.GetObject<List<string>>("HardLink");
            if (hardLinks == null || hardLinks.Count == 0)
            {
                session.Log("RemoveHardLinksDeferred: No hardlinks passed via CustomActionData");
                return ActionResult.NotExecuted;
            }
            for (int i = 0; i < hardLinks.Count; i++)
            {
                var destination = hardLinks[i];
                session.Log(string.Format("RemoveHardLinksDeferred: Destination:{0}", destination));
                if (File.Exists(destination))
                {
                    try
                    {
                        File.Delete(destination);
                    }
                    catch (Exception ex)
                    {
                        session.Log("RemoveHardLinksDeferred: Exception caught for File.Delete:" + ex);
                    }
                    session.Log("RemoveHardLinksDeferred: The file '{0}' successfully deleted", destination);
                }
            }
            return ActionResult.Success;
        }
    }
}
